| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390 |
- 'use client';
- import './style.scss';
- import Link from 'next/link';
- import Image from 'next/image';
- import { redirect, useRouter } from 'next/navigation';
- import { useState, useEffect, useCallback, MouseEvent } from 'react';
- import { Menu } from 'lucide-react';
- import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
- import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
- import { faBookmark as nBookmark, faThumbsUp as nThumbsUp, faThumbsDown as nThumbsDown, faFlag as nFlag, faPenToSquare, faTrashCan } from '@fortawesome/free-regular-svg-icons';
- import { faQrcode, faPrint, faLink, faShareNodes, faBookmark as yBookmark, faThumbsUp as yThumbsUp, faThumbsDown as yThumbsDown, faFlag as yFlag } from '@fortawesome/free-solid-svg-icons';
- import Loading from '@/app/component/Loading';
- import Comment from '@/app/(forum)/comment/view';
- import LatestList from '@/app/(forum)/post/_component/LatestPosts';
- import BoardResponse from '@/dtos/response/forum/board/boardResponse';
- import PostResponse from '@/dtos/response/forum/post/postResponse';
- import PostReactionRequest from '@/dtos/request/forum/post/postReactionRequest';
- import PostBookmarkRequest from '@/dtos/request/forum/post/postReactionRequest';
- import Content from '../_component/Content';
- import QRCode from '../_component/QRCode';
- import Copied from '../_component/Copied';
- import SnsShare from '../_component/SnsShare';
- import Report from '../_component/Report';
- import { Reaction } from '@/constants/forum';
- import { fetchPostReaction, fetchPostBookmark, fetchPostDelete } from '@/lib/api/forum/post';
- import { getDateTime, throwError, formatDate, isDateOverdue } from '@/lib/utils/client';
- import useAuth from '@/hooks/useAuth';
- type Props = {
- _board: BoardResponse,
- _post: PostResponse
- };
- export default function View({ _board, _post }: Props)
- {
- useEffect(() => {
- // 신고 횟수 초과 게시글은 접근 불가
- if (_post.reports > _board.boardMeta.view.blameHideCount && _board.boardMeta.view.blameHideCount > 0) {
- alert('비공개 게시글입니다.');
- redirect(`/board/${_post.boardCode}${window.location.search}`);
- }
- }, []);
- const router = useRouter();
- const { member, isLogined } = useAuth();
- const [error, setError] = useState<string>('');
- const [loading, setLoading] = useState<boolean>(false);
- const [qrCode, setQrCode] = useState<boolean>(false);
- const [copied, setCopied] = useState<boolean>(false);
- const [snsShare, setSnsShare] = useState<boolean>(false);
- const [report, setReport] = useState<boolean>(false);
- const [hasLike, setHasLike] = useState<boolean>(_post.hasLike);
- const [hasDisLike, setHasDisLike] = useState<boolean>(_post.hasDislike);
- const [hasBookmark, setHasBookmark] = useState<boolean>(_post.hasBookmark);
- const [hasReport, setHasReport] = useState<boolean>(_post.hasReport);
- useEffect(() => {
- if (error) {
- alert(error);
- setError('');
- }
- }, [error]);
- const toggleQRCode = useCallback((e: MouseEvent<HTMLAnchorElement>) => {
- e.preventDefault();
- setQrCode((prev) => !prev);
- }, []);
- const handlePrint = useCallback(() => {
- window.print();
- }, []);
- const toggleCopied = useCallback((e: MouseEvent<HTMLAnchorElement>) => {
- e.preventDefault();
- setCopied((prev) => !prev);
- }, []);
- const toggleSnsShare = useCallback((e: MouseEvent<HTMLAnchorElement>) => {
- e.preventDefault();
- setSnsShare((prev) => !prev);
- }, []);
- // 좋아요/싫어요
- const handleReaction = useCallback(async (e: MouseEvent<HTMLButtonElement>) => {
- const reaction = Number(e.currentTarget.value);
- if (!await isLogined()) {
- return;
- }
- fetchPostReaction({ postID: _post.id, reaction: reaction } as PostReactionRequest).then(res => {
- if (res.ok) {
- switch (reaction) {
- case Reaction.Like:
- setHasLike(!hasLike);
- setHasDisLike(false);
- break;
- case Reaction.Dislike:
- setHasDisLike(!hasDisLike);
- setHasLike(false);
- break;
- }
- } else {
- throwError(res);
- }
- }).catch((err) => {
- setError(err.message);
- }).finally(() => {
- setLoading(false);
- });
- }, [member, hasLike, hasDisLike]);
- // 즐겨찾기
- const handleBookmark = useCallback(async () => {
- if (!await isLogined()) {
- return;
- }
- fetchPostBookmark({ postID: _post.id } as PostBookmarkRequest).then(res => {
- if (res.ok) {
- setHasBookmark(!hasBookmark);
- } else {
- throwError(res);
- }
- }).catch((err) => {
- setError(err.message);
- }).finally(() => {
- setLoading(false);
- });
- }, [member, hasBookmark]);
- // 신고하기 시작
- const handleReport = useCallback(async () => {
- if (hasReport) {
- alert('이미 신고하셨습니다.');
- return;
- }
- if (!await isLogined()) {
- return;
- }
- setReport((prev) => !prev);
- }, [member, hasReport]);
- // 수정하기
- const handleEdit = useCallback(async () => {
- if (!await isLogined()) {
- return;
- }
- // 게시글 삭제 보호 확인
- if (_board.boardMeta.general.allowUpdateProtection && !member?.isAdmin) {
- if (isDateOverdue(_post.createdAt, _board.boardMeta.general.updateProtectionDays)) {
- return alert(`게시글 작성 후 ${_board.boardMeta.general.updateProtectionDays}일이 지나 수정이 불가능합니다.`);
- }
- }
- router.push(`/post/edit/${_post.id}`);
- }, [member]);
- // 게시글 삭제
- const handleDelete = useCallback(async () => {
- if (!await isLogined()) {
- return;
- }
- // 게시글 삭제 보호 확인
- if (_board.boardMeta.general.allowDeleteProtection && !member?.isAdmin) {
- if (isDateOverdue(_post.createdAt, _board.boardMeta.general.deleteProtectionDays)) {
- return alert(`게시글 작성 후 ${_board.boardMeta.general.deleteProtectionDays}일이 지나 삭제가 불가능합니다.`);
- }
- }
- if (confirm('정말 삭제하시겠습니까?')) {
- fetchPostDelete(_post.id).then(res => {
- if (res.ok) {
- alert('게시글이 삭제되었습니다.');
- router.push(`/board/${_post.boardCode}`);
- } else {
- throwError(res);
- }
- }).catch((err) => {
- setError(err.message);
- }).finally(() => {
- setLoading(false);
- });
- }
- }, [member]);
- return (
- <div id='postView'>
- {loading && <Loading />}
- <QRCode isEnable={true} open={qrCode} onChange={setQrCode} />
- <Copied isEnable={true} open={copied} onChange={setCopied} />
- <SnsShare isEnable={true} open={snsShare} onChange={setSnsShare} />
- <Report isEnable={true} open={report} onChange={setReport} onComplete={setHasReport} postID={_post.id} memberID={member?.id} />
- {/* 글 제목 */}
- <section className='subject whitespace-normal break-words'>
- {_post.boardPrefixID && ("[" + _post.boardPrefix.name + "]")} {_post.subject}
- </section>
- <hr />
- {/* 글 작성자/작성일시/부가기능들 */}
- <section className='attribution'>
- {_board.boardMeta.view.showMemberPhoto && (
- <div>
- <article className='writer-thumb'>
- <Image src='/resources/thumb.gif' alt='회원 사진' width={84} height={0} />
- </article>
- </div>
- )}
- <div>
- <article className='writer-info'>
- <ul>
- <li>※ {_post.writer.name}</li>
- {_board.boardMeta.view.showMemberRegDate && <li>{formatDate(_post.writer.createdAt)} 가입</li>}
- {_board.boardMeta.view.showMemberSummary && <li>{_post.writer.summary}</li>}
- </ul>
- </article>
- <article className='post-info'>
- <ul>
- <li>조회: {_post.views}</li>
- <li>댓글: {_post.comments}</li>
- {_board.boardMeta.view.allowLike && (
- <li>좋아요: {_post.likes}</li>
- )}
- {_board.boardMeta.view.allowDislike && (
- <li>싫어요: {_post.dislikes}</li>
- )}
- <li>IP: {_post.ipAddress}</li>
- </ul>
- </article>
- <article className='post-date'>
- 작성일시 : {getDateTime(_post.createdAt)}
- </article>
- <article className='functions'>
- <ul>
- {_board.boardMeta.view.allowPostUrlQrCode && (
- <li>
- <a href='#' rel='noreferrer' onClick={toggleQRCode}><FontAwesomeIcon icon={faQrcode} /> QR</a>
- </li>
- )}
- {_board.boardMeta.view.allowPrint && (
- <li>
- <a href='#' rel='noreferrer' onClick={handlePrint}><FontAwesomeIcon icon={faPrint} /> 인쇄</a>
- </li>
- )}
- {_board.boardMeta.view.allowPostUrlCopy && (
- <li>
- <a href='#' rel='noreferrer' onClick={toggleCopied}><FontAwesomeIcon icon={faLink} /> 주소</a>
- </li>
- )}
- {_board.boardMeta.view.allowSnsShare && (
- <li>
- <a href='#' rel='noreferrer' onClick={toggleSnsShare}><FontAwesomeIcon icon={faShareNodes} /> 공유</a>
- </li>
- )}
- </ul>
- </article>
- </div>
- </section>
- <hr />
- {/* 글 내용 */}
- <section className='content'>
- <Content boardMeta={_board.boardMeta} content={_post.content}></Content>
- {_post.tagList.length > 0 && (
- <article>
- {/* 태그 표시 */}
- {_post.tagList.map((row, i) => (
- <span key={i}>
- <Link href={`/tag/${row.slug}`}>#{row.slug}</Link>
- </span>
- ))}
- </article>
- )}
- </section>
- <hr />
- {/* 제어 버튼들 */}
- <section className='controls'>
- <article>
- <Link href={`/board/${_post.boardCode}${window.location.search}`} className='btn btn-default'>목록</Link>
- {_board.boardMeta.view.allowPrevNextBotton && (
- <>
- {!!_post.prevID && (
- <Link href={`/post/${_post.prevID}`} className='btn btn-default'>이전</Link>
- )}
- {!!_post.nextID && (
- <Link href={`/post/${_post.nextID}`} className='btn btn-default'>다음</Link>
- )}
- </>
- )}
- </article>
- <article className='functions'>
- {_board.boardMeta.view.allowLike && (
- <div className='hidden sm:block'>
- <button className='btn btn-default' title='좋아요' value={Reaction.Like} onClick={handleReaction}>
- <FontAwesomeIcon icon={!hasLike ? nThumbsUp : yThumbsUp } />
- </button>
- </div>
- )}
- {_board.boardMeta.view.allowDislike && (
- <div className='hidden sm:block'>
- <button className='btn btn-default' title='싫어요' value={Reaction.Dislike} onClick={handleReaction}>
- <FontAwesomeIcon icon={!hasDisLike ? nThumbsDown : yThumbsDown } />
- </button>
- </div>
- )}
- {_board.boardMeta.view.allowBookmark && (
- <div className='hidden md:block'>
- <button className='btn btn-default' onClick={handleBookmark}>
- <FontAwesomeIcon icon={!hasBookmark ? nBookmark : yBookmark } /> 즐겨찾기
- </button>
- </div>
- )}
- {_board.boardMeta.view.allowBlame && (
- <div className='hidden xlm:block'>
- <button className='btn btn-default' onClick={handleReport}>
- <FontAwesomeIcon icon={!hasReport ? nFlag : yFlag } /> 신고
- </button>
- </div>
- )}
- <div className='hidden xl:block'>
- <button className='btn btn-default' onClick={handleEdit}>수정</button>
- </div>
- <div className='hidden xl:block'>
- <button className='btn btn-default' onClick={handleDelete}>삭제</button>
- </div>
- <div className='block xl:hidden'>
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <button className='btn btn-default' title='더보기'>
- <Menu className='w-5 h-5' />
- </button>
- </DropdownMenuTrigger>
- <DropdownMenuContent align='end'>
- {_board.boardMeta.view.allowLike && (
- <DropdownMenuItem className='block sm:hidden'>
- <button type="button" value={Reaction.Like} onClick={handleReaction}><FontAwesomeIcon icon={!hasLike ? nThumbsUp : yThumbsUp } /> 좋아요</button>
- </DropdownMenuItem>
- )}
- {_board.boardMeta.view.allowDislike && (
- <DropdownMenuItem className='block sm:hidden'>
- <button type="button" value={Reaction.Dislike} onClick={handleReaction}><FontAwesomeIcon icon={!hasDisLike ? nThumbsDown : yThumbsDown } /> 좋아요</button>
- </DropdownMenuItem>
- )}
- {_board.boardMeta.view.allowBookmark && (
- <DropdownMenuItem className='block md:hidden' onClick={handleBookmark}><FontAwesomeIcon icon={!hasBookmark ? nBookmark : yBookmark } /> 즐겨찾기</DropdownMenuItem>
- )}
- {_board.boardMeta.view.allowBlame && (
- <DropdownMenuItem className='block xlm:hidden' onClick={handleReport}><FontAwesomeIcon icon={!hasReport ? nFlag : yFlag } /> 신고</DropdownMenuItem>
- )}
- <DropdownMenuItem className='block xl:hidden' onClick={handleEdit}><FontAwesomeIcon icon={faPenToSquare} /> 수정</DropdownMenuItem>
- <DropdownMenuItem className='block xl:hidden' onClick={handleDelete}><FontAwesomeIcon icon={faTrashCan} /> 삭제</DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
- </div>
- </article>
- </section>
- <br/>
- {/* 댓글 */}
- <Comment board={_board} post={_post} />
- {/* 게시판 최근 글 */}
- <LatestList boardListMeta={_board.boardMeta.list} boardID={_board.id} boardCode={_board.code} postID={_post.id} />
- </div>
- );
- }
|